// default configuration_file

#include "std_options.h"


//#include <boost/lexical_cast.hpp>


// 
//  std lib
// 
#include <ios>
#include <stdexcept>
	using std::runtime_error;
#include <algorithm>
	using std::max;
#include <functional>
	using std::bind2nd;
	using std::equal_to;
#include <boost/tuple/tuple.hpp>
	using boost::tuple;
#include <vector>
	using std::vector;
#include <string>
	using std::string;
#include <iostream>
	using std::cerr;
	using std::ostream;
#include <boost/format.hpp>
	using boost::format;
#include <boost/regex.hpp>
	using boost::regex;
	using boost::regex_match;
	using boost::smatch;
#include <sstream>
	using std::ostringstream;

// 
//  boost
// 
#include <boost/filesystem.hpp>
	using boost::filesystem::path;
#include <boost/algorithm/string.hpp>
	using boost::split;
#include <boost/shared_ptr.hpp>
	using boost::shared_ptr;
#include <boost/foreach.hpp>

namespace po = boost::program_options;
namespace fs = boost::filesystem;

#include "indent_output_filter.h"
	using indent_streams::indent_with_prefix_and_timestamp;
	using indent_streams::indent_with_prefix_and_timestamp_output_filter;
	using indent_streams::indent_with_prefix;
	using indent_streams::indent_with_prefix_output_filter;

namespace io = boost::iostreams;
#include <boost/iostreams/device/null.hpp>
	using boost::iostreams::null_sink;
#include <boost/iostreams/device/file.hpp>
	using boost::iostreams::file_sink;

#include <boost/program_options/errors.hpp>
	using boost::program_options::validation_error;
#include <boost/program_options/parsers.hpp>
	using boost::program_options::parse_config_file;
#include <boost/program_options.hpp>
#include <boost/any.hpp>


#include "indent_str.h"
string wrap_by_farthest_colon(const string& old_str, unsigned line_len, unsigned indent_sz = 4, unsigned post_colon_len = 30);


t_std_options::t_std_options(const std::string& exe_description_, const string& program_usage_, const string& version_string_, bool use_summary_file, unsigned max_line_length_)
:
		max_line_length (max_line_length_)		,
		using_summary_file(false)				,
		using_log_file    (false)				,
		exe_description	(exe_description_)		,
		program_usage   (program_usage_)		,
		version_string	(version_string_)		,
		cmdline_display_args("", max_line_length),
		verbosity		(0),
		default_optional(new boost::program_options::options_description("Default options")) 	,
		default_config_file(new boost::program_options::options_description("Config file only"))	,
		optional		("Options")				,
		hidden  		("Hidden options") 		,
		mandatory		("Mandatory options") 
{
	default_optional->add_options()
		("help,h", 						"Display this help message and exit.");

	// only version if version string
	if (version_string.length())
		default_optional->add_options()
			("version,V", 				"Display version information and exit");

	default_optional->add_options()
		("log_file,L",				po::value(&file_name_log)
										//->set_name("FILE")
										,"Write log to FILE.")
		("skip_parameter_logging",  	"Do not print program parameters to log file.")
		("verbose,v",				po::value(option_level(&verbosity))->zero_tokens(),
										"Print more verbose messages at each additional verbosity level.")
		("configuration_file",		po::value< string>()
										 //->set_name("FILE"),
										 ,"Command line options will override options set in any configuration file.\n"
											 "Defaults to '<PROGRAM_NAME>.ini'");

	default_config_file->add_options()
		("log_file,L",				po::value(&file_name_log)
										//->set_name("FILE"),
										,"Write log to FILE.")
		("skip_parameter_logging",  	"Do not print program parameters to log file.")
		("verbose,v",				po::value<unsigned>(&verbosity),
										"Print more verbose messages at each additional verbosity level.");
	if (use_summary_file)
	{
		default_optional->add_options()
				("summary_file,S",	po::value<string>(&file_name_summary)
										//->set_name("FILE"),
										,"Write summary to FILE.");
		default_config_file->add_options()
				("summary_file,S",	po::value<string>(&file_name_summary)
										//->set_name("FILE"),
										,"Write summary to FILE.");
	}


	// set up verbose stream and null stream
	strm_verbose.push(indent_with_prefix_output_filter(indent_with_prefix("    ")), 0U);

	strm_null.push(null_sink());
}


//________________________________________________________________________________________
// 
// 		open_summary_file()
//________________________________________________________________________________________
std::ostream&		t_std_options::open_summary_file()
{
	// Return previously opened
	if (using_summary_file)
		return strm_summary;


	if (file_name_summary.empty())
		strm_summary.push(null_sink());
	else
		strm_summary.push(file_sink(file_name_summary, std::ios_base::app | std::ios_base::out));

	using_summary_file = true;
	return strm_summary;
}

//________________________________________________________________________________________
// 
// 		open_log_file()
//________________________________________________________________________________________
std::ostream&		t_std_options::open_log_file()
{
	// Return previously opened
	if (using_log_file)
		return strm_log;

	// No name: null stream
	if (file_name_log.empty())
		strm_log.push(null_sink());
	else
	{
		// indent with time stamp and program name
		strm_log.push(indent_with_prefix_and_timestamp_output_filter(indent_with_prefix_and_timestamp(program_file_name)), 0U);
		strm_log.push(file_sink(file_name_log, std::ios_base::app | std::ios_base::out),0U);
	}

	using_log_file = true;
	return strm_log;
}

//________________________________________________________________________________________
// 
// 		open_log_file()
//________________________________________________________________________________________
std::ostream&		t_std_options::open_both()
{
	// Return previously opened
	if (!using_log_file)
		open_log_file();

	p_device_both.reset(new filtered_tee_device(strm_verbose, strm_log));
	p_strm_both.reset(new filtered_tee_stream(*p_device_both, 0));

	return *p_strm_both;
}




//________________________________________________________________________________________
// 
// 		validate unsigned and option_level
//________________________________________________________________________________________
void validate(boost::any& v,
              const std::vector<std::string>& values,
              t_option_level* target_type, int)
{
    using namespace boost::program_options;

	t_option_level i;
	if (!v.empty())
		i = boost::any_cast<t_option_level>(v);
	v = boost::any(i.inc());
}


// validate unsigned
void validate(boost::any& v, 
              const std::vector<std::string>& values,
              unsigned* target_type, int)
{

	using namespace boost::program_options;
	using boost::lexical_cast;
	using boost::any;

    // Make sure no previous assignment to 'a' was made.
    validators::check_first_occurrence(v);
	
    // Extract the first string from 'values'. If there is more than
    // one string, it's an error, and exception will be thrown.
    const string& s = validators::get_single_string(values);

	// match a negative sign an a number
	static boost::regex r("(\\-)(\\d+)");
	
    // Do regex match and convert the interesting part to 
    // int.
	boost::smatch what;
    if (regex_match(s, what, r)) 
	{
		if (what[1].matched)
		{
			throw validation_error(validation_error::invalid_option_value, "This should not be a negative number.");
		}
        v = any(lexical_cast<unsigned>(what[2]));
    } 
	else 
	{
        throw validation_error(validation_error::invalid_option_value, "\"" + s + "\" is not a valid value.");
    }        
}

// allow multiple bool options?
void validate(boost::any& v, const std::vector<std::string>& values,
				t_option_level* target_type, int);


// validate unsigned
void validate(boost::any& v, 
              const std::vector<std::string>& values,
              unsigned* target_type, int);


//________________________________________________________________________________________
// 
// 		check_required_options
//________________________________________________________________________________________
void check_required_options(const po::variables_map& vm, const po::options_description& desc)
{
	BOOST_FOREACH(boost::shared_ptr<po::option_description> opt, desc.options()) 
	{
		if (!vm.count(opt->long_name()))
			throw boost::program_options::error("Missing option [--" + opt->long_name() + "]");
	}
}



//________________________________________________________________________________________
// 
// 		process_command_line
//________________________________________________________________________________________
bool t_std_options::process_command_line(int argc, char* argv[])
{
	//
	//  Save Original command line
	// 
	string orig_command_line = argv[0];
	for (int ii = 1; ii < argc; ++ii)
		orig_command_line += string (" ") + argv[ii];

	program_name = string(argv[0]);
	program_file_name = fs::path(program_name).filename().native();

	//
	//	Add default arguments after user added ones 
	// 
	//  bug in replace extension
	string program_config_file_name;
	if (!fs::path(program_file_name).has_extension())
		program_config_file_name = program_file_name + ".ini";
	else
		program_config_file_name = fs::path(program_name).replace_extension(".ini").native();
	//std::cerr << program_name << "\n";
	//std::cerr << program_file_name << "\n";
	//std::cerr << program_config_file_name << "\n";
	BOOST_FOREACH(const boost::shared_ptr<po::option_description>& i, default_optional->options()) 
	{
		if (i->long_name() == "configuration_file")
		{
			optional.add_options()
				("configuration_file",		po::value< string>()
												 //->set_name("FILE"),
												 ,	("Command line options will override options set in any configuration file.\n"
													 "Defaults to '" + program_config_file_name + "'.").c_str());
		}
		else 
			optional.add(i);
	}

	// 
	// for display
	//
	if (mandatory.options().size())
		cmdline_display_args.add(mandatory);
	cmdline_display_args.add(optional);

	// 
	// for cmd_line
	//
	po::options_description cmdline_args("");
	cmdline_args.add(optional);
	cmdline_args.add(mandatory);
	cmdline_args.add(hidden);
		
	// parse command line
	po::variables_map vm;
	po::command_line_parser clp(argc, argv);
	clp.options(cmdline_args);
	if (positional_options.max_total_count())
		clp.positional(positional_options);
	po::store(clp.run(), vm);

	//
	//  print version
	// 
	if (vm.count("version"))
	{
		print_version(cerr);
		return false;
	}
	
	//
	//  print help
	// 
	if (vm.count("help"))
	{
		//cerr << cmdline_display_args << "\n";
		print_usage(cerr);
		return false;
	}


	//
	//	Cannot use variable because notify not yet called 
	// 
	if (vm.count("configuration_file") )
		program_config_file_name = vm["configuration_file"].as<string>();

	// Die if log file name = program file name !!
	if (program_config_file_name == program_name)
		throw boost::program_options::error((format("Please choose a valid configuration file different from the program name. [%1$s]") % 
												program_config_file_name).str());

	//
	//	Add default arguments after user added ones 
	// 
	BOOST_FOREACH(const boost::shared_ptr<po::option_description>& i, default_config_file->options()) 
		config_file.add(i);

	// Don't waste time if default_config_file options empty
	if (config_file.options().size())
	{
		if (fs::exists(fs::path(program_config_file_name)))
			store(po::parse_config_file<char>(program_config_file_name.c_str(), config_file), vm);
		else
		{
			// user specified config file should exist
			// default config file can be absent...
			if (vm.count("configuration_file") )
				throw boost::program_options::error((format("Missing configuration file. [%1$s]") % 
															program_config_file_name).str());
		}
	}

	//
	//  store everything and callbacks
	//		Checking of mandatory options here
	//
	notify(vm);

	check_required_options(vm, mandatory);

	//
	//  whether anything is printed
	// 
	if (verbosity)
		strm_verbose.push(cerr, 0);
	else
		strm_verbose.push(null_sink());

	if (!vm.count("skip_parameter_logging"))
	{
		log() << string(120, '=') << "\n";
		log() << orig_command_line << "\n";
	}

	return true;
}



//________________________________________________________________________________________
// 
// 		print_version
//________________________________________________________________________________________
void t_std_options::print_version(ostream& os)
{
	os << "\n    " << program_file_name
		//<< exe_file_name << "    Version " + version + "\n\n";
	<< wrap_by_farthest_colon("   Version " + version_string, max_line_length, 4) << "\n";
}


//
//	Each line of exe_description is indented with 2 spaces
// 	Lines have further hanging indentation.
//  If a line break is desired with hanging indentation, use \1 instead of \n
//  Tabs are 4 spaces
//
//________________________________________________________________________________________
// 
// 		print_usage
//________________________________________________________________________________________
void t_std_options::print_usage(ostream& os)
{
	// get option descriptions
	ostringstream sstrm;
	sstrm << cmdline_display_args;

	// wrap and indent exe_description
	vector<string> lines;
	boost::split(lines, exe_description, std::bind2nd(std::equal_to<char>(), '\n'));
	for (unsigned i = 0; i < lines.size(); ++i)
	{
		std::replace(lines[i].begin(),  lines[i].end(),  '\1',  '\n');
		// tabs to strings
		boost::replace_all(lines[i], "\t", string(4, ' '));
		lines[i] = "  " + hanging_indent_with_line_wrap(lines[i], 6, max_line_length - 2);
	}
	string wrapped_exe_description;
	for (unsigned i = 0; i < lines.size(); ++i)
		wrapped_exe_description += lines[i] + '\n';

	os
		<< string(max_line_length, '-') << "\n\n"
		<< "Usage:\n"
   		<< hanging_indent_with_line_wrap(	"  " + program_file_name + " " + program_usage + "\n",
											program_file_name.length() + 3, max_line_length)
		<< sstrm.str()
	//wrap_by_farthest_colon(sstrm.str(), max_line_length)
		<< "\nDescription:\n"
		<< wrapped_exe_description
		<< "\n"
		<< string(max_line_length, '-') << "\n\n";

}



void parse_indices(const string& search_str, vector<unsigned>& indices)
{

	boost::regex expression("^(\\d+)(?:\\s*\\-\\s*(\\d+))?[\\s,]*");
	boost::smatch what;
	string::const_iterator b = search_str.begin();
	string::const_iterator e = search_str.end();
	while(regex_search(b, e, what, expression))
	{

		if (what[2].matched)
		{
			unsigned end = atol(what[2].str().c_str());
			unsigned beg = atol(what[1].str().c_str());
			if (end >= beg)
				for (unsigned i = beg; i < end + 1; ++i)
					indices.push_back(i);
			else
				throw boost::program_options::error("Incorrectly specified index range \"" +
														what[0].str() +
													"\".\nIndex ranges must be specified as \"A-B\" "
														"where A <= B.");
		}
		else
			indices.push_back(atol(what[1].str().c_str()));
		b = what[0].second;
	}
//	sort(indices.begin(),  indices.end());
//	remove_duplicates(indices);
	if (b != e)
		throw boost::program_options::error("Indices should be separated by commas, "
											"with ranges denoted by hyphens.\n"
											"e.g.  \"1,3,9,11-13\".");
}


//8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888

//		todo

//8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888










std::pair<unsigned, unsigned> get_max_and_last_line_len(const string& str)
{
	vector<string> lines;
	boost::split(lines, str, bind2nd(equal_to<char>(), '\n'));
	if (!lines.size())
		lines.push_back(str);
	unsigned len = lines[0].length();
	BOOST_FOREACH(const string& s, lines) 
		if (len < s.length())
			len = s.length();
	return std::make_pair(len, lines.back().length());
}

string wrap_by_farthest_colon(const string& old_str, unsigned line_len, unsigned indent_sz, unsigned post_colon_len)
{
	assert(line_len >= post_colon_len);
	string str = old_str;

	// tabs to strings
	boost::replace_all(str, "\t", string(indent_sz, ' '));

	// split into separate lines
	vector<string> lines;
	boost::split(lines, str, std::bind2nd(std::equal_to<char>(), '\n'));

	//
	// find furthest colon positions
	//

	vector<string::size_type> colon_line_len(lines.size(), string::npos);
	vector<string::size_type> colon_pos(lines.size(), string::npos);

	// furthest position of colon. All colons will be forced to align here
	string::size_type max_colon_pos = 0;
	for (unsigned i = 0; i < lines.size(); ++i)
	{
		// continue if no colon
		colon_pos[i] = colon_line_len[i] = lines[i].find(": ");
		if (colon_line_len[i] == string::npos)
			continue;

		// Colon position reasonable. Save
		if (colon_line_len[i] < line_len - post_colon_len)
		{
			max_colon_pos = max(max_colon_pos, colon_line_len[i]);
			continue;
		}

		//
		// colon position too far: line too long. Wrap before colon
		//

		// Remove post colon part which does not need to be line wrapped
		string post_colon_part = lines[i].substr(colon_pos[i] + 2);

		// remove any excessive spaces before colon
		lines[i].erase(colon_pos[i]);
		lines[i].erase(lines[i].find_last_not_of(' ') + 1);
		lines[i] += ": ";

		// wrap pre colon line
		lines[i] = hanging_indent_with_line_wrap(lines[i], 8, line_len - post_colon_len - 2);

		unsigned max_line_len, last_line_len;
		boost::tie(max_line_len, last_line_len) =get_max_and_last_line_len(lines[i]);

		// the maximum colon position is the length of the longest line + 2
		max_colon_pos = max(max_colon_pos, (std::size_t)max_line_len );

		// remember where the line lengths are
		colon_pos[i] = lines[i].length() - 2;
		colon_line_len[i] = last_line_len - 2;

		// add back non-wrapped post colon part of string
		lines[i] += post_colon_part;
	}

	// add spaces to align colons
	for (unsigned i = 0; i < lines.size(); ++i)
		if (colon_line_len[i] != string::npos && max_colon_pos != colon_line_len[i])
		{
			lines[i].insert(colon_pos[i], max_colon_pos - colon_line_len[i], ' ');
		}



	// for each line, split into sub-lines and wrap those by hanging indent past the
	// 		colon position
	for (unsigned i = 0; i < lines.size(); ++i)
	{
		// get string last newline and handle separately

		string::size_type last_newline = lines[i].rfind("\n");
		string wrap_part = lines[i].substr(last_newline + 1);
		lines[i].erase(last_newline + 1);


		// split by \1
		vector<string> sub_lines;
		boost::split(sub_lines, wrap_part, bind2nd(equal_to<char>(), '\1'));
		if (!sub_lines.size())
			sub_lines.push_back(wrap_part);


		// wrap the first line
		sub_lines[0] = hanging_indent_with_line_wrap(sub_lines[0], max_colon_pos + 2, line_len);


		// wrap the subsequent lines after indenting past the colon position
		for (unsigned j = 1; j < sub_lines.size(); ++j)
			sub_lines[j] = hanging_indent_with_line_wrap(string(max_colon_pos + 2, ' ') + sub_lines[j],
										max_colon_pos + 2, line_len);

		// join back up
		//lines[i].clear();
		for (unsigned j = 0; j < sub_lines.size(); ++j)
			lines[i] += sub_lines[j] + "\n";
	}

	// join back up
	str.clear();
	for (unsigned i = 0; i < lines.size(); ++i)
		str += lines[i];

	return str;
}



